<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="3c4e0a25" id="3c4e0a25"><span data-lake-id="u84e2919e" id="u84e2919e">典型回答</span></h1>
  <p data-lake-id="u98c9f81e" id="u98c9f81e"><span data-lake-id="u9c5606f2" id="u9c5606f2">​</span><br></p>
  <p data-lake-id="u5f729bcf" id="u5f729bcf"><strong><span data-lake-id="u1549631a" id="u1549631a">我们在Java中遇到的比较严重的问题应该就是OutOfMemoryError，StackOverflowError等这些问题了，那么，如果遇到这些问题，JVM一定会退出吗？</span></strong></p>
  <p data-lake-id="u7d44e989" id="u7d44e989"><strong><span data-lake-id="u0cf64691" id="u0cf64691">​</span></strong><br></p>
  <p data-lake-id="u9e8ea002" id="u9e8ea002"><span data-lake-id="uda5156da" id="uda5156da">我们知道，JVM是一个操作系统的进程，而在Linux和其他类Unix操作系统中，当一个进程在执行非法内存访问时，如访问未分配给它的内存或者访问超出其允许范围的内存时，操作系统会向该程序发送SIGSEGV信号（“段错误”（Segmentation Fault）），若进程没有注册信号处理函数会直接退出，并产生</span><code data-lake-id="ue2ccabab" id="ue2ccabab"><span data-lake-id="u34746e1f" id="u34746e1f">Segment Fault</span></code><span data-lake-id="ubfc2c506" id="ubfc2c506">错误提示。</span></p>
  <p data-lake-id="uf6ad0605" id="uf6ad0605"><strong><span data-lake-id="u71a61f99" id="u71a61f99">​</span></strong><br></p>
  <p data-lake-id="ueac0472e" id="ueac0472e"><span data-lake-id="u72609c17" id="u72609c17">而我们熟知的OutOfMemoryError，StackOverflowError就是</span><code data-lake-id="ueffa3e34" id="ueffa3e34"><span data-lake-id="u755d8b5f" id="u755d8b5f">Segment Fault</span></code><span data-lake-id="u966480a7" id="u966480a7">的具体情况，不过，JVM被设计成能够容忍和隔离单个线程出现问题，当一个线程崩溃时，JVM会尝试将问题限定在该线程内，而不会影响其他线程或整个应用程序。</span></p>
  <p data-lake-id="u37c8320e" id="u37c8320e"><br></p>
  <p data-lake-id="u5cdde6c7" id="u5cdde6c7"><strong><span data-lake-id="u6429fb59" id="u6429fb59">也就是说，即使我们的线程执行过程中，发生了OutOfMemoryError，StackOverflowError等这些问题了，也并不代表JVM就一定要立即退出或者崩溃。</span></strong></p>
  <p data-lake-id="ua61365ab" id="ua61365ab"><strong><span data-lake-id="ua3efec75" id="ua3efec75">​</span></strong><br></p>
  <p data-lake-id="u302c6ead" id="u302c6ead"><span data-lake-id="ua6c406d9" id="ua6c406d9">主要是因为，OutOfMemoryError，StackOverflowError等这些我们看到的ERROR，已经是JVM在注册了SIGSEGV信号处理函数之后，经过自己的处理之后抛给我们的错误了。（这部分源码在文末）</span></p>
  <p data-lake-id="ue64190b8" id="ue64190b8"><strong><span data-lake-id="uc61ca806" id="uc61ca806">​</span></strong><br></p>
  <p data-lake-id="u19fcde4e" id="u19fcde4e"><strong><span data-lake-id="ue78f231c" id="ue78f231c">而OutOfMemoryError，StackOverflowError等这些错误跑给我们之后，其实都是可以被catch的，如果被catch掉之后，程序还是可以正常执行，而不会崩溃退出的。</span></strong></p>
  <p data-lake-id="u4be9d2ed" id="u4be9d2ed"><br></p>
  <p data-lake-id="uf7f95108" id="uf7f95108"><span data-lake-id="u28383ae1" id="u28383ae1">所以说，Java中的所有线程的崩溃，包括主线程、子线程，并不是说一定就会导致JVM直接崩溃的。</span></p>
  <p data-lake-id="u165a3470" id="u165a3470"><br></p>
  <h2 data-lake-id="b3a8e5e2" id="b3a8e5e2"><span data-lake-id="u567e6fd5" id="u567e6fd5">扩展知识</span></h2>
  <p data-lake-id="uf9368fb6" id="uf9368fb6"><br></p>
  <h2 data-lake-id="c4eb3d79" id="c4eb3d79"><span data-lake-id="udeace600" id="udeace600">Linux为何会对访问内存错误发出异常信号？</span></h2>
  <p data-lake-id="u872958ce" id="u872958ce"><br></p>
  <p data-lake-id="uac1a0a7f" id="uac1a0a7f"><span data-lake-id="u5f361ecb" id="u5f361ecb">Linux通过MMU（内存管理单元）将物理内存转换为虚拟内存，确保每个进程能看到的地址空间都是相同的，并且是自己进程独享的。</span></p>
  <p data-lake-id="ue26d76d9" id="ue26d76d9"><span data-lake-id="u4f8982eb" id="u4f8982eb">​</span><br></p>
  <p data-lake-id="ud75f016e" id="ud75f016e"><span data-lake-id="ucdd586c3" id="ucdd586c3">32位下每个进程可访问从0到4G内存（用户空间3G，内核1G）。但实际上物理内存是所有进程共享的，所以内核需要有能力防止进程随意破坏不属于自己的内存空间。</span></p>
  <p data-lake-id="uec30be18" id="uec30be18"><span data-lake-id="u4947c373" id="u4947c373"><br>
    使用C语言执行如下代码：</span></p>
  <p data-lake-id="u617eedce" id="u617eedce"><br></p>
  <pre lang="java"><code>
int main(int argc,char **argv){
    int *p =(int *)0xC0000FEE; 
    *p = 1024;
    return 0;
}
</code></pre>
  <p data-lake-id="u49fb98a9" id="u49fb98a9"><br></p>
  <p data-lake-id="u75b4ab29" id="u75b4ab29"><span data-lake-id="u88afa771" id="u88afa771">以上代码由于非法向进程的内核地址空间</span><code data-lake-id="u07dbd038" id="u07dbd038"><span data-lake-id="u0b3a482b" id="u0b3a482b">0xC0000FEE</span></code><span data-lake-id="u8e719700" id="u8e719700">处写入数据1024，操作系统会为该进程发出SIGSEGV的信号，由于我们没有实现信号处理函数，所以进程会默认被系统杀死，并向控制台输出</span><code data-lake-id="u41057d60" id="u41057d60"><span data-lake-id="u5281094d" id="u5281094d">Segment Fault</span></code><span data-lake-id="u7839d25a" id="u7839d25a">错误。</span></p>
  <p data-lake-id="ue363e484" id="ue363e484"><br></p>
  <h2 data-lake-id="d77944a8" id="d77944a8"><span data-lake-id="ubdcccae5" id="ubdcccae5">进程、主线程、子线程的关系</span></h2>
  <p data-lake-id="ude7511b8" id="ude7511b8"><br></p>
  <p data-lake-id="ua2848c61" id="ua2848c61"><span data-lake-id="ucd2bd2a0" id="ucd2bd2a0">在Linux下通过fork函数创建进程，fork创建进程时会将调用者（父进程）的页表、文件系统、打开文件句柄等信息复制一份，这是一个成本较大的操作，所以引入线程。</span></p>
  <p data-lake-id="uf0a2eb72" id="uf0a2eb72"><span data-lake-id="u413f4441" id="u413f4441">​</span><br></p>
  <p data-lake-id="uc0565d6d" id="uc0565d6d"><span data-lake-id="u96abd60a" id="u96abd60a">Linux的线程也是通过fork函数实现，其入参与进程略有不同（CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND），这样使得所有子线程与进程共享页表、文件系统、打开句柄等信息，降低fork系统调用的成本。</span></p>
  <p data-lake-id="u50b890fe" id="u50b890fe"><span data-lake-id="u7629f7d6" id="u7629f7d6"><br>
    进程和线程在Linux内核中都被抽象为一个task_struct的结构体，其中包括了页表、栈信息、文件描述符等很多信息，其中tgid保存了线程组id，如果tgid为pid则是所谓的主线程，而所有子线程tgid就是子线程的pid。本质上主线程和子线程没有任何区别（都是进程fork出来的，共享的信息都一样）。</span></p>
  <p data-lake-id="ued8d832f" id="ued8d832f"><span data-lake-id="u0237b46c" id="u0237b46c"><br>
    我们看到线程的fork参数中有一个CLONE_SIGHAND，代表线程与进程共享信号处理函数。所以</span><strong><span data-lake-id="u39c0334b" id="u39c0334b">JVM内部在启动时注册的信号处理函数，会被所有线程复用。JVM注册的新号处理函数会抛出各种Error和Exception，但不会结束JVM进程。这样子线程崩溃了，也不会影响JVM中的其他线程。</span></strong></p>
  <p data-lake-id="ue38c9c82" id="ue38c9c82"><br></p>
  <h2 data-lake-id="13b41117" id="13b41117"><span data-lake-id="u08ec6aa4" id="u08ec6aa4">JVM都处理了哪些异常信号？</span></h2>
  <p data-lake-id="u4e25ca4f" id="u4e25ca4f"><br></p>
  <p data-lake-id="u34257d74" id="u34257d74"><span data-lake-id="uf8ca7a8b" id="uf8ca7a8b">除了上面提到的SIGSEGV信号，JVM还注册了如下信号处理函数：</span></p>
  <table data-lake-id="a8a14a81" id="a8a14a81" margin="true" class="lake-table" style="width: 750px">
   <colgroup>
    <col width="250">
    <col width="250">
    <col width="250">
   </colgroup>
   <tbody>
    <tr data-lake-id="ucc67aabb" id="ucc67aabb">
     <td data-lake-id="u9cf939d7" id="u9cf939d7">
      <p data-lake-id="u55d30f83" id="u55d30f83"><span data-lake-id="udf7a7269" id="udf7a7269">信号值</span></p></td>
     <td data-lake-id="u39f018b1" id="u39f018b1">
      <p data-lake-id="ude35db58" id="ude35db58"><span data-lake-id="u1deedb17" id="u1deedb17">信号名称</span></p></td>
     <td data-lake-id="u1e4316b6" id="u1e4316b6">
      <p data-lake-id="uda752a03" id="uda752a03"><span data-lake-id="u6fc3b2ee" id="u6fc3b2ee">作用</span></p></td>
    </tr>
    <tr data-lake-id="u5acf758c" id="u5acf758c">
     <td data-lake-id="u5f419e01" id="u5f419e01">
      <p data-lake-id="u902f8dbf" id="u902f8dbf"><span data-lake-id="u67c96a32" id="u67c96a32">11</span></p></td>
     <td data-lake-id="u256ca643" id="u256ca643">
      <p data-lake-id="uf1218db6" id="uf1218db6"><span data-lake-id="u435d990f" id="u435d990f">SIGSEGV</span></p></td>
     <td data-lake-id="uad2c9c61" id="uad2c9c61">
      <p data-lake-id="ubabdb982" id="ubabdb982"><span data-lake-id="ufb9f21d5" id="ufb9f21d5">试图访问未分配给自己的内存，或试图往没有写权限的内存地址写数据</span></p></td>
    </tr>
    <tr data-lake-id="u3a075f04" id="u3a075f04">
     <td data-lake-id="u5b89aff3" id="u5b89aff3">
      <p data-lake-id="uc5d58a70" id="uc5d58a70"><span data-lake-id="u0a8801a4" id="u0a8801a4">13</span></p></td>
     <td data-lake-id="ud98a380e" id="ud98a380e">
      <p data-lake-id="u440c9362" id="u440c9362"><span data-lake-id="ud8248d77" id="ud8248d77">SIGPIPE</span></p></td>
     <td data-lake-id="u90e8e44a" id="u90e8e44a">
      <p data-lake-id="u78f7ff0c" id="u78f7ff0c"><span data-lake-id="u64c1246b" id="u64c1246b">管道破裂。这个信号通常在进程间通信产生，比如采用 FIFO (管道) 通信的两个进程，读管道没打开或者意外终止就往管道写，写进程会收到 SIGPIPE 信号。此外用 Socket 通信的两个进程，写进程在写 Socket 的时候，读进程已经终止。</span></p></td>
    </tr>
    <tr data-lake-id="uc04816b3" id="uc04816b3">
     <td data-lake-id="u46cee807" id="u46cee807">
      <p data-lake-id="u81b6bddf" id="u81b6bddf"><span data-lake-id="u71abc11b" id="u71abc11b">7</span></p></td>
     <td data-lake-id="ubc07553e" id="ubc07553e">
      <p data-lake-id="u3ab1ecb9" id="u3ab1ecb9"><span data-lake-id="ue32c4177" id="ue32c4177">SIGBUS</span></p></td>
     <td data-lake-id="ucb99fe48" id="ucb99fe48">
      <p data-lake-id="u3a60b4fc" id="u3a60b4fc"><span data-lake-id="u299b6965" id="u299b6965">非法地址，包括内存地址对齐 (alignment) 出错。比如访问一个四个字长的整数，但其地址不是 4 的倍数。它与 SIGSEGV 的区别在于后者是由于对合法存储地址的非法访问触发的 (如访问不属于自己存储空间或只读存储空间)。</span></p></td>
    </tr>
    <tr data-lake-id="u56e26f8c" id="u56e26f8c">
     <td data-lake-id="ub5162d2d" id="ub5162d2d">
      <p data-lake-id="u82cbdf83" id="u82cbdf83"><span data-lake-id="u7ba9780f" id="u7ba9780f">4</span></p></td>
     <td data-lake-id="ub9d999c7" id="ub9d999c7">
      <p data-lake-id="ucc95b000" id="ucc95b000"><span data-lake-id="ub0452e36" id="ub0452e36">SIGILL</span></p></td>
     <td data-lake-id="uc83a2694" id="uc83a2694">
      <p data-lake-id="ud639e53d" id="ud639e53d"><span data-lake-id="u0c5308fa" id="u0c5308fa">执行了非法指令。通常是因为可执行文件本身出现错误，或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。</span></p></td>
    </tr>
    <tr data-lake-id="u9c2e2776" id="u9c2e2776">
     <td data-lake-id="u6f6fd3d4" id="u6f6fd3d4">
      <p data-lake-id="u5d509a97" id="u5d509a97"><span data-lake-id="u9b09eb0f" id="u9b09eb0f">8</span></p></td>
     <td data-lake-id="uf320a4a3" id="uf320a4a3">
      <p data-lake-id="u71c3013d" id="u71c3013d"><span data-lake-id="ub10b1d74" id="ub10b1d74">SIGFPE</span></p></td>
     <td data-lake-id="u08737f49" id="u08737f49">
      <p data-lake-id="ue9e2254f" id="ue9e2254f"><span data-lake-id="uc8ac9de2" id="uc8ac9de2">在发生致命的算术运算错误时发出。不仅包括浮点运算错误，还包括溢出及除数为 0 等其它所有的算术的错误。</span></p></td>
    </tr>
    <tr data-lake-id="u6c68391e" id="u6c68391e">
     <td data-lake-id="u46f6d45f" id="u46f6d45f">
      <p data-lake-id="u3fca6fd6" id="u3fca6fd6"><span data-lake-id="u26f67931" id="u26f67931">25</span></p></td>
     <td data-lake-id="u4eb01e39" id="u4eb01e39">
      <p data-lake-id="ud3b2e6fa" id="ud3b2e6fa"><span data-lake-id="u1e0c4269" id="u1e0c4269">SIGXFSZ</span></p></td>
     <td data-lake-id="u4c76ff37" id="u4c76ff37">
      <p data-lake-id="u2fd2b31f" id="u2fd2b31f"><span data-lake-id="u91ad2b9e" id="u91ad2b9e">当进程企图扩大文件以至于超过文件大小资源限制。</span></p></td>
    </tr>
   </tbody>
  </table>
  <p data-lake-id="u423af806" id="u423af806"><br></p>
  <p data-lake-id="uab8af824" id="uab8af824"><span data-lake-id="u0e649784" id="u0e649784">JVM拦截这些信号的目的有三个：</span></p>
  <p data-lake-id="udb013558" id="udb013558"><br></p>
  <ol list="ub9c1659d">
   <li fid="u902cbe30" data-lake-id="uf7a9e1ba" id="uf7a9e1ba"><span data-lake-id="ud5f36d4b" id="ud5f36d4b">将异常转换成Error、Exception等信息抛给用户，如：StackOverFlowError、OutOfMemoryError、NullPointException等。</span></li>
   <li fid="u902cbe30" data-lake-id="u90a05c92" id="u90a05c92"><span data-lake-id="u954fd66b" id="u954fd66b">在对系统产生致命错误之前做一些保护性动作。</span></li>
   <li fid="u902cbe30" data-lake-id="u2a214e02" id="u2a214e02"><span data-lake-id="uf2049fc9" id="uf2049fc9">防止操作系统杀死JVM进程。</span></li>
  </ol>
  <p data-lake-id="uf1fa91cc" id="uf1fa91cc"><br></p>
  <p data-lake-id="u3d735489" id="u3d735489"><span data-lake-id="u8fdb7a1d" id="u8fdb7a1d">对于其他未注册信号处理函数的异常信号，JVM会退出，并生成</span><code data-lake-id="u1472acf1" id="u1472acf1"><span data-lake-id="u6557f33d" id="u6557f33d">hs_err_pid_xxx.log</span></code><span data-lake-id="u41df8a75" id="u41df8a75"> crash文件用于分析异常。</span></p>
  <p data-lake-id="u8eb812bd" id="u8eb812bd"><br></p>
  <h2 data-lake-id="27078d53" id="27078d53"><span data-lake-id="u796b0689" id="u796b0689">talk is cheap，show me the code</span></h2>
  <p data-lake-id="u4f735288" id="u4f735288"><br></p>
  <p data-lake-id="u771a165f" id="u771a165f"><span data-lake-id="u62479c56" id="u62479c56">最后我们从JVM的入口点向下查找注册信号处理函数的代码，由于篇幅原因省略无关代码，以下为jdk8-b08的源码，不同jdk版本信号处理逻辑略有不同。</span></p>
  <p data-lake-id="u5ca4559b" id="u5ca4559b"><br></p>
  <pre lang="java"><code>
// jdk/jdk/src/share/bin/main.c
int
main(int argc, char ** argv)
{
    int margc;
    char** margv;
    const jboolean const_javaw = JNI_FALSE;

    margc = argc;
    margv = argv;
#endif /* JAVAW */

// JLT_Launch函数开始初始化JVM虚拟机
    return JLI_Launch(margc, margv,
                   sizeof(const_jargs) / sizeof(char *), 
                   ...);
}
</code></pre>
  <p data-lake-id="u5b6f57e3" id="u5b6f57e3"><br></p>
  <pre lang="java"><code>
// jdk/jdk/src/share/bin/java.c
int
JLI_Launch(int argc, char ** argv,              /* main argc, argc */
        int jargc, const char** jargv,          /* java args */
      ...
)
{
 //省略无关代码..
    if (!LoadJavaVM(jvmpath, &amp;ifn)) { //加载JVM虚拟机
        return(6);
    }
 // ...
}
</code></pre>
  <p data-lake-id="ue1b2facb" id="ue1b2facb"><br></p>
  <pre lang="java"><code>
// jdk/hotspot/src/os/posix/launcher/java_md.c
jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
    //省略无关代码..
    //通过动态加载lib库的形式创建JVM
    ifn-&gt;CreateJavaVM = (CreateJavaVM_t)
      dlsym(libjvm, "JNI_CreateJavaVM");
    // ...
}
</code></pre>
  <p data-lake-id="u60160aea" id="u60160aea"><br></p>
  <pre lang="java"><code>
// jdk/hotspot/src/share/vm/prims/jni.cpp
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
  HS_DTRACE_PROBE3(hotspot_jni, CreateJavaVM__entry, vm, penv, args);
    //省略无关代码..
      result = Threads::create_vm((JavaVMInitArgs*) args, &amp;can_try_again); //创建JVM线程
    //...
 }
</code></pre>
  <p data-lake-id="u32aaef6b" id="u32aaef6b"><br></p>
  <pre lang="java"><code>
 // jdk/hotspot/src/share/vm/runtime/thread.cpp
 jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
  //省略无关代码..
  // Initialize the os module after parsing the args
  jint os_init_2_result = os::init_2(); //调用与操作系统有关的init_2方法，不同操作系统实现略有不同
  //...
}
</code></pre>
  <p data-lake-id="ue097a532" id="ue097a532"><br></p>
  <pre lang="java"><code>
// jdk/hotspot/src/os/linux/vm/os_linux.cpp
jint os::init_2(void)
{
    //省略无关代码..
    Linux::signal_sets_init(); //初始化信号集
    Linux::install_signal_handlers(); //注册信号处理函数，这里是重点
    //...
}

void os::Linux::install_signal_handlers() {
  if (!signal_handlers_are_installed) {
    signal_handlers_are_installed = true;
    //省略无关代码..
    //以下是注册上面表格中列出的信号
    set_signal_handler(SIGSEGV, true);
    set_signal_handler(SIGPIPE, true);
    set_signal_handler(SIGBUS, true);
    set_signal_handler(SIGILL, true);
    set_signal_handler(SIGFPE, true);
    set_signal_handler(SIGXFSZ, true);
   //...
   }
 }
 
void os::Linux::set_signal_handler(int sig, bool set_installed) {
 //省略无关代码..
  struct sigaction sigAct;
  sigfillset(&amp;(sigAct.sa_mask));
  sigAct.sa_handler = SIG_DFL;
  if (!set_installed) {
    sigAct.sa_flags = SA_SIGINFO|SA_RESTART;
  } else {
    sigAct.sa_sigaction = signalHandler; //注册信号处理函数，这里是重点
    sigAct.sa_flags = SA_SIGINFO|SA_RESTART;
  }
    //...
}

extern "C" JNIEXPORT int
JVM_handle_linux_signal(int signo, siginfo_t* siginfo,
                        void* ucontext, int abort_if_unrecognized); //声明信号处理逻辑，由外部文件导入

void signalHandler(int sig, siginfo_t* info, void* uc) {
  assert(info != NULL &amp;&amp; uc != NULL, "it must be old kernel");
  JVM_handle_linux_signal(sig, info, uc, true); //执行信号处理逻辑
}
</code></pre>
  <p data-lake-id="ue5cd97ce" id="ue5cd97ce"><br></p>
  <pre lang="java"><code>
// jdk/hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp
extern "C" JNIEXPORT int
JVM_handle_linux_signal(int sig,
                        siginfo_t* info,
                        void* ucVoid,
                        int abort_if_unrecognized) {
    //这个函数包含了最终被信号处理函数拦截执行的逻辑，比较长，这里摘出栈溢出、打印crash文件的代码
    
    //以下是StackOverFlowError的异常处理流程，这里可以看到JVM会根据当前栈空间范围和线程类型thread_state()为用户代码创建的线程，抛出StackOverFlowError
    if (sig == SIGSEGV) {
      address addr = (address) info-&gt;si_addr;

      // check if fault address is within thread stack
      if (addr &lt; thread-&gt;stack_base() &amp;&amp;
          addr &gt;= thread-&gt;stack_base() - thread-&gt;stack_size()) {
        // stack overflow
        if (thread-&gt;in_stack_yellow_zone(addr)) {
          thread-&gt;disable_stack_yellow_zone();
          if (thread-&gt;thread_state() == _thread_in_Java) {
            // Throw a stack overflow exception.  Guard pages will be reenabled
            // while unwinding the stack.
            stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::STACK_OVERFLOW);
          } else {
            // Thread was in the vm or native code.  Return and try to finish.
            return 1;
          }
        }  //省略其他分支
    }
    //...
    //上面省略很多代码，在函数最后会打crash文件hs_err_pid_xxx.log
    VMError err(t, sig, pc, info, ucVoid);
    err.report_and_die();
  }
</code></pre>
 </body>
</html>